TD DSA 2021 de Antoine Ly - rapport de Fabien Faivre
4. Modélisation¶
4.1. Setup¶
import shap
shap.initjs()
4.1.1. Chargement des données¶
# On Importe les données
#df
df_train=pd.read_parquet('/mnt/data/interim/df_train.gzip')
df_val=pd.read_parquet('/mnt/data/interim/df_val.gzip')
df_test=pd.read_parquet('/mnt/data/interim/df_test.gzip')
#X
X_train=pd.read_parquet('/mnt/data/interim/X_train.gzip')
X_val=pd.read_parquet('/mnt/data/interim/X_val.gzip')
X_test=pd.read_parquet('/mnt/data/interim/X_test.gzip')
X_train_prepro=pd.read_parquet('/mnt/data/interim/X_train_prepro.gzip')
X_val_prepro=pd.read_parquet('/mnt/data/interim/X_val_prepro.gzip')
X_test_prepro=pd.read_parquet('/mnt/data/interim/X_test_prepro.gzip')
#y
y_train=pd.read_parquet('/mnt/data/interim/y_train.gzip')
y_val=pd.read_parquet('/mnt/data/interim/y_val.gzip')
y_test=pd.read_parquet('/mnt/data/interim/y_test.gzip')
pd.options.display.max_colwidth=300
4.2. Modélisation¶
4.2.1. Création du code générique¶
On commence par définir une fonction générique qui sera en capacité d’ajuster, optimiser et logger dans MLFlow les résultats de pipelines qui seront produits pour chaque essai
Le mode de fonctionnement souhaité consiste à
1- définir un pipeline au sens de sklearn
2- utiliser une fonction générique pour ajuster le pipeline (éventuellement en optimisant les paramètres) et en stocker le résultat dans MLFlow
4.2.1.1. Préalables : création des fonctions de résultat souhaitées¶
La première étape consiste à construire une fonction générique qui calculera les scores du pipeline que nous souhaitons suivre.
Dans le cas présent comme l’exercice de classification est multiclasse, nous sommes intéressés par les f1, precision et recall calculés avec l’option macro qui réalise une moyenne des résultats obtenus par classe.
def score_estimator(
estimator, X_train, X_test, df_train, df_test, target_col
):
"""
Evalue un pipeline sur le jeu de train et test avec plusieurs métriques
Ici les métriques utilisées sont :
- f1 macro
- precision macro
- recall macro
INPUTS :
- estimator : un pipeline
- X_train, X_test, df_train, df_test : les DataFrames contenant les jeux de données et test
- target_col : le nom de la colonne cible dans les df
OUTPUTS :
- un DataFrame avec les métriques calculées sur les jeux de train et test fournis
"""
metrics = [
("f1_macro", f1_score),
("precision_macro", precision_score),
("recall_macro", recall_score),
]
res = []
for subset_label, X, df in [
("train", X_train, df_train),
("test", X_test, df_test),
]:
y = df[target_col]
y_pred = estimator.predict(X)
for score_label, metric in metrics:
score = metric(y, y_pred, average='macro')
res.append(
{"subset": subset_label, "metric": score_label, "score": score}
)
res = (
pd.DataFrame(res)
.set_index(["metric", "subset"])
.score.unstack(-1)
.round(4)
.loc[:, ['train', 'test']]
)
return res
Pour pouvoir stocker les scores dans MLFlow, on les convertit en dictionnaires
def scores_to_dict(score_df):
d = score_df['train'].to_dict()
d1 = dict(zip([x+'_train_' for x in list(d.keys())], list(d.values())))
d = score_df['test'].to_dict()
d2 = dict(zip([x+'_test' for x in list(d.keys())], list(d.values())))
d1.update(d2)
return d1
Création d’une fonction affichant une matrice de confusion
def plot_cm(y_test, y_pred, target_names=[-1, 0, 1],
figsize=(5,3)):
"""Create a labelled confusion matrix plot."""
cm = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=figsize)
sns.heatmap(cm, annot=True, fmt='g', cmap='BuGn', cbar=False,
ax=ax)
ax.set_title('Confusion matrix')
ax.set_xlabel('Predicted')
ax.set_xticklabels(target_names)
ax.set_ylabel('Actual')
ax.set_yticklabels(target_names,
fontdict={'verticalalignment': 'center'});
4.2.1.2. Création de la fonction d’entraînement générique¶
La fonction suivante est celle qui sera systématiquement appélée pour entraîner les pipelines
Tip
L’évaluation fianle des modèles se faisant sur base de f1-macro dans le TD, c’est la métrique que nosu avons retenue pour la partie optimisation de la fonction générique
def trainPipelineMlFlow(mlf_XP,
xp_name_iter,
pipeline,
X_train, y_train, X_test, y_test,
target_col='sentiment',
fixed_params={},
use_opti=False, iterable_params={}, n_iter=20):
"""
Fonction générique permettant d'entrainer et d'optimiser un pipeline sklearn
Les paramètres et résultats sont stockés dans MLFlow
INPUTS:
- mlf_XP : nom de l'experiment à créer dans MLFlow
- xp_name_iter : nom du run créé dans l'experiment de MLFlow
- pipeline : un pipeline au sens ed sklearn
- X_train, y_train, X_test, y_test : des dataframes contenant les jeux d'entrainement et de test
- target_col : le nom de la colonne du DataFrame y qui constitue la cible
- fixed_params : un dictionnaire contenant les paramètres fixes dont l'utilisateur souhaite fixer la valeur dans le pipeline
- use_opti : boolean, est-ce qu'une optimisation est recherchée. Si oui, utilisera RandomizedSearchCV
- iterable_params : un dictionnaire contenant les nom des paramètres ciblés du pipeline et des listes contenant les valeusr possibles
- n_iter : le nombre d'itérations maximales à réaliser par RandomizedSearchCV
FONCTIONNEMENT:
stocke dans MLFlow :
- le pipeline entrainé
- les principaux paramètres correspondant aux paramètres fixes et aux éventuels paramètres optimaux après RandomizedSearchCV
- les scores (scalaires) calculés par la fonction score_estimator
- le temps d'exécution
imprime :
- le nom de l'experiment
- le pipeline entraîné
- les paramètres principaux (cf FONCTIONNEMENT)
- la matrice de confusion du pipeline sur le jeu de test fourni en entrée
OUTPUTS:
- le pipeline entraîné
"""
mlflow.set_experiment(mlf_XP)
with mlflow.start_run(run_name=xp_name_iter):
start_time = time.monotonic()
warnings.filterwarnings("ignore")
# fit pipeline
pipeline.set_params(**fixed_params)
if not use_opti:
search = pipeline
else:
search = RandomizedSearchCV(estimator = pipeline,
param_distributions = iterable_params,
n_jobs = -1,
cv = 5,
scoring = 'f1_macro',
n_iter = n_iter,
random_state = 42
)
search.fit(X_train, y_train[target_col])
# get params
params_to_log = fixed_params #select initial params
if use_opti:
params_to_log.update(search.best_params_) #update for optimal solution
mlflow.log_params(params_to_log)
# Evaluate metrics
y_pred=search.predict(X_test)
score = score_estimator(estimator=search,
X_train=X_train,
X_test=X_test,
df_train=y_train,
df_test=y_test,
target_col=target_col
)
# Print out metrics
print('XP :', xp_name_iter, '\n')
print('pipeline : \n', search, '\n')
print("params: \n", params_to_log, '\n')
print('scores : \n', score, '\n')
print("Test confusion matrix: \n")
plot_cm(y_test, search.predict(X_test))
#r Report to MlFlow
mlflow.log_metrics(scores_to_dict(score))
mlflow.sklearn.log_model(pipeline, xp_name_iter)
end_time = time.monotonic()
elapsed_time = timedelta(seconds=end_time - start_time)
print('elapsed time :', elapsed_time)
mlflow.set_tag(key="elapsed_time", value=elapsed_time)
return search;
4.2.1.3. Utilitaires : pour faciliter l’utilisation des pipelines¶
Si les pipelines permettent un traitement souple et homogène entre les jeux de données, leur manipulation n’est pas évidente. Notamment, le libellé des paramètres peut vide devenir délicat et difficilement lisible avec une combinaison de nom d’étape et du nom du paramètre dans l’étape du pipeline. La fonction suivante permet de rechercher tous les paramètres d’un pipeline qui contiennent une chaine de caractère spécifique.
def target_params(pipe, dict_keyval):
"""
Crée un dictionnaire constitué de tous les paramètres incluant 'pattern' d'un pipe et leur assigne une valeur unique
"""
res={}
for key in list(dict_keyval.keys()):
target = "[a-zA-Z\_]+__" + key
rs = re.findall(target, ' '.join(list(pipe.get_params().keys())))
rs=dict.fromkeys(rs, dict_keyval[key])
res.update(rs)
return res
4.2.1.4. Utilitaires : Adaptation des pipelines¶
La cellule suivante permet de créer des étapes de sélection de colonnes dans les Data Frame en entrée
from sklearn.base import BaseEstimator, TransformerMixin
class TextSelector(BaseEstimator, TransformerMixin):
def __init__(self, field):
self.field = field
def fit(self, X, y=None):
return self
def transform(self, X):
if isinstance(X,(list,pd.core.series.Series,np.ndarray)): #permet d'avoir une structure souple si l'input n'est pas un DataFrame. Permet notamment d'utiliser LIME
return X
else:
return X[self.field]
4.2.1.5. Utilitaires : Visualisation¶
def plotWc(text, stopwords=None, title=''):
wc = WordCloud(
stopwords=stopwords,
width=800,
height=400,
max_words=1000,
random_state=44,
background_color="white",
collocations=False
).generate(text)
plt.figure(figsize = (10,10))
plt.imshow(wc, interpolation="bilinear")
plt.axis("off")
plt.title(title)
plt.show()
4.2.2. Approche initiale¶
On commence par construire un modèle simple qui nous servira de modèle de base que nous chercherons à améliorer.
Warning
Dans cette première étape, nous travaillerons sur le jeu train que nous avon découpé et évaluerons ses performances sur le jeu val.
Seuls les principaux modèles seront réentrainés sur { train + val } avant d’être évalués sur le véritable jeu test
On suit les modèles dans un DataFrame résultats
résultats = pd.DataFrame(columns=['modèle', 'f1_macro_val'])
résultats
| modèle | f1_macro_val |
|---|
4.2.2.1. Bag of Words avec Random Forest¶
Dans cette expérimentation, nous créons un modèle simple :

La classe TfidfVectorizer de sklearn permet d’appliquer ou non le traitement TF-IDF et dans ce dernier cas de travailler directement avec un Bag of Words
tfidf_RF_pipeline = Pipeline(
steps=[
('coltext', TextSelector('text')),
("tfidf", TfidfVectorizer()),
("classifier", RandomForestClassifier(n_jobs=-1)),
]
)
Déjà dans cet exemple simple, le nombre de paramètres est important et leur nom vite complexe :
list(tfidf_RF_pipeline.get_params().keys())
['memory',
'steps',
'verbose',
'coltext',
'tfidf',
'classifier',
'coltext__field',
'tfidf__analyzer',
'tfidf__binary',
'tfidf__decode_error',
'tfidf__dtype',
'tfidf__encoding',
'tfidf__input',
'tfidf__lowercase',
'tfidf__max_df',
'tfidf__max_features',
'tfidf__min_df',
'tfidf__ngram_range',
'tfidf__norm',
'tfidf__preprocessor',
'tfidf__smooth_idf',
'tfidf__stop_words',
'tfidf__strip_accents',
'tfidf__sublinear_tf',
'tfidf__token_pattern',
'tfidf__tokenizer',
'tfidf__use_idf',
'tfidf__vocabulary',
'classifier__bootstrap',
'classifier__ccp_alpha',
'classifier__class_weight',
'classifier__criterion',
'classifier__max_depth',
'classifier__max_features',
'classifier__max_leaf_nodes',
'classifier__max_samples',
'classifier__min_impurity_decrease',
'classifier__min_impurity_split',
'classifier__min_samples_leaf',
'classifier__min_samples_split',
'classifier__min_weight_fraction_leaf',
'classifier__n_estimators',
'classifier__n_jobs',
'classifier__oob_score',
'classifier__random_state',
'classifier__verbose',
'classifier__warm_start']
En première intention on ajuste le pipeline sur le jeu d’entraînement avant les étapes de preprocessing réalisées lors de l’EDA
pipe = tfidf_RF_pipeline
base_tfidf_RF_= trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "base_tfidf_RF",
pipeline = pipe,
X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42})
);
XP : base_tfidf_RF
pipeline :
Pipeline(steps=[('coltext', TextSelector(field='text')),
('tfidf', TfidfVectorizer(use_idf=False)),
('classifier',
RandomForestClassifier(n_jobs=-1, random_state=42))])
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42}
scores :
subset train test
metric
f1_macro 0.9991 0.6781
precision_macro 0.9991 0.7071
recall_macro 0.9991 0.6676
Test confusion matrix:
elapsed time : 0:00:03.469991
Le modèle de base produit un f1 macro de 67,81% sur le jeu de validation avec le paramétrage par défaut de sklearn. On observe le très fort f1 macro sur le jeu d’entraînement qui indique un fort surapprentissage. L’intérêt de ce pipeline est d’être très rapide à l’entraînement (à peine plus de 3 secondes ici)
On voit bien que la difficulté viendra de la classe neutre qui peut facilement être confondue avec les classes négatives ou positives
Par contre, il est plus surprenant de voir des tweets positifs classés en négatifs et inversement.
La suite investigue ce phénomène
pipe = base_TfIdf_RF_
y_val_pred = pipe.predict(X_val)
exemples_realNeg_predPos = X_val[(y_val['sentiment']==-1) & (y_val_pred==1)]
exemples_realPos_predNeg = X_val[(y_val['sentiment']==1) & (y_val_pred==-1)]
4.2.2.1.1. Analyse des tweets négatifs classé positifs¶
exemples_realNeg_predPos
| text | |
|---|---|
| 22050 | oo noo thats not good |
| 22195 | Legs are hurting because I was standing up all day. |
| 22218 | Is beastypops tired? I wish i was. My tablets are just making me want to throw up! |
| 22278 | BAH, you`be made me all hungry now |
| 22349 | just tried DMing you but it tried to download some strange file so stopped! How come no gmail MaccyM? Missing you SADS!! |
| ... | ... |
| 27195 | i wish i could but it would cost too much to call you all the way from the UK |
| 27219 | I can`t believe you tweeted that. It was our special moment |
| 27329 | unfortunetly no I wish, I mean sometimes like twice a yr they`ll have a party, but not always |
| 27364 | I bet you received lots of hit from that tweet; at work i cannot, wish i could |
| 27475 | wish we could come see u on Denver husband lost his job and can`t afford it |
119 rows × 1 columns
plotWc(" ".join(exemples_realNeg_predPos['text']), stopwords=stopwords.words('english'), title = "Wordcloud des tweets négatifs prédits positifs")
On voit les limites des approches de type Bag of Words : le modèle est trompé par des mots à connotation positive sans en comprendre l’enchaînement
4.2.2.1.2. Analyse des tweets positifs classé négatifs¶
exemples_realPos_predNeg
| text | |
|---|---|
| 22078 | dont worry im not!!! i dont get it on my tv |
| 22165 | Bummer I know LOL I actually do more partying when i am in school then out of school..I think it somehow helps me..hahaha! |
| 22281 | _22 ok so I`m having a complete insomniac moment. It`s 6am(almost) and I`m STILL awake. I hate when I can`t stop thinking! mornin! |
| 22319 | OMG I`M SOO EXCITED! i`ve been waiting for it ever since i saw the 5th one at midnight the night before! |
| 22630 | let me guess ... ran a few miles? Respect dude, I can`t do it. Maybe you should train me |
| 22636 | I`m gonna havta temp stop fllwing u while ur talkin abt kobe bc I loveeeeeeee him & I`m taking it personal and I like lebron 2. |
| 23235 | That poor girl on britains got talent, god love her forgot the words and cried but gets a second chance to perform again : ] |
| 23259 | Gmorning ooh giirll, Mondays |
| 23499 | Chilling feeling really nice.. |
| 23500 | Hey, nothing wrong with that! |
| 23536 | I gave up cable in these tough economic times. it was either cable or shoes, and you know what cable lost |
| 23610 | _mcc bye, Scotty! i`m gonna miss you. ily<333 |
| 23695 | _Honi Might be cute to do a little picture book called 'The little book of boring' |
| 23746 | aww i hope it does fly by because JT episodes are usually really good (and it`s early but so far this ep hassn`t disappointed) |
| 23857 | i want one so bad get one for me ?? (: |
| 23860 | I want some pineapple! I miss my baby |
| 23864 | on that note - i do not feel missed. |
| 23883 | congrats to the A`s!! ugh, we still have til the end of june |
| 23886 | let me know how it goes I`m praying. Ummmph. I still can`t believe it. |
| 23915 | is feeling good.. kinda tired.. miss him... can`t wait for grad this weekend!! |
| 24016 | morning world, is raining 2day so revision don`t seem so tough, |
| 24191 | Good luck with the footage - none of the stations are breaking in live with it |
| 24413 | Hello there! Thankyou. I always seem to make a difference in someone`s life everyday. |
| 24840 | The Beatles? Those scousers with funny haircuts? More talent in The Banana Splits! |
| 24979 | is so excited for this summer... Steve Winwood and Eric Clapton, Eagles of Death Metal, and ahhh the Dead Weather |
| 25120 | Yes, Cathy. (Ordinarily, I don`t have much of a problem with naked girls chasing me, however! ) |
| 25192 | i do i do, i feel absoulutley fine |
| 25367 | I really like Lady GaGa`s 'Paparazzi'... #whatshappening |
| 25389 | fair enough. actually, you dont have to give me x-men. mad max will do me fine |
| 25440 | Romantic evening in with papa murphys and 'battle BC' from the history channel |
| 25559 | No nothing wrong with thinking he`s hot, but he belongs to me |
| 25891 | don`t be worried! I`m safe and sound! <3 you! |
| 26025 | What`s up yall! I made it an early night! I think ima bout to take a shower and chill witcha! Did I miss anything? |
| 26149 | baaha & healthy choice my friend! (: |
| 26318 | Hey! It`s so funny - I stopped to get a coffee on the way in and spilt half of it in the car! |
| 26436 | awww the wee gril in britains got talent |
| 26494 | Hahahaha! It`s not horrible, if others were singing with I`m sure it could work. I wish I could afford my own drum set |
| 26519 | Its raining cats and dogs here, in Mysore! Thankfully, no pigs/swines! |
| 26574 | Work, work, work. Finally not sick, though. |
| 26665 | I`m so bunged up!! I Hate colds!! |
| 26673 | I`ve burnt my collerbone and arms and face! aww! |
| 26683 | I wish I was going to Internet Week |
| 26870 | is problem free for now. atleast i already said to that person the truth. |
| 26880 | Feeling inspired this evening, huh? |
| 27046 | is having a well-deserved break today..NO PHONE CALLS, NO EMAILS..only plenty of catch up movies to doooooo |
| 27067 | No, i dont think its bad. And its very well edited, too. |
| 27111 | i want so bad to go to the mcfly`s concert |
| 27190 | friday night is my fav night of the week but now I have to go to stupid dog training classes |
| 27212 | Aww chamber callbacks... Soo emotional |
| 27225 | Gonna run to the gym to get my workout in before my really f`kin big pile of mulch arrives at around 8 or 9am! I`m excited about my mulch |
| 27323 | is liking this feeling |
| 27427 | i hate my presentation hahah whatever im glad its over |
| 27473 | So I get up early and I feel good about the day. I walk to work and I`m feeling alright. But guess what... I don`t work today. |
On observe deux phénomènes contraires :
vraisemblablement des problèmes de labelisation (ex 22281 classé positifs…)
des tweets manifestements positifs sans pièges et pourtant classés négatifs (25367)
plotWc(" ".join(exemples_realPos_predNeg['text']), stopwords=stopwords.words('english'), title = "Wordcloud des tweets positifs prédits négatifs")
pour essayer de comprendre ce qu’il s’est passé sur l’instance 22238, on peut essayer d’analyser le résultat à partir de Lime :
exemples_realPos_predNeg['text'][25367]
"I really like Lady GaGa`s 'Paparazzi'... #whatshappening"
from lime import lime_text
from lime.lime_text import LimeTextExplainer
explainer = LimeTextExplainer(class_names=['negative', 'neutral', 'positive'])
exp = explainer.explain_instance(exemples_realPos_predNeg['text'][25367], base_TfIdf_RF_.predict_proba, top_labels=1)
exp.show_in_notebook(text=True)
Le résultat est très surprenant. On peut imaginer que really est utilisé plus frequemment dans des tweets négatifs
def list_words_with(text_series, search='', nb=30):
'''
Cette fonction permet de lister les mots dans un string qui contiennent une certaine chaîne de caractères
inputs :
- text_series : un pd.Series contennat les chaînes de caractères
- search : la séquence à rechercher
- nb : ressortir les nb occurences les plus fréquentes
output :
- une liste de tuples contenant
+ le mot contenant la séquence recherchée
+ le nombre d'occurence dans text_series
'''
#searchPattern = f"\w*{search}\w*"
searchPattern = f"\w*{search}\w* "
cnt = Counter()
for tweet in text_series:
# Replace all URls with 'URL'
tweet = re.findall(searchPattern,tweet)
for word in tweet:
cnt[word] += 1
return cnt.most_common(nb)
list_words_with(X_train['text'][y_train['sentiment']==-1], 'really')
[('really ', 240), ('reallyreally ', 1), ('reallyyyyy ', 1), ('reallyy ', 1)]
list_words_with(X_train['text'][y_train['sentiment']==1], 'really')
[('really ', 202)]
Ce qui est bien le cas
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_ | 0.669789 |
4.2.2.2. variante preprocessing¶
On peut juger de l’intérêt des prétraitements que nous avons réalisés en changeant le jeu d’entrée :

pipe = tfidf_RF_pipeline
base_TfIdf_RF_prepro_= trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "base_TfIdf_RF_prepro",
pipeline = pipe,
X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42})
);
XP : base_TfIdf_RF_prepro
pipeline :
Pipeline(steps=[('coltext', TextSelector(field='text')),
('tfidf', TfidfVectorizer(use_idf=False)),
('classifier',
RandomForestClassifier(n_jobs=-1, random_state=42))])
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42}
scores :
subset train test
metric
f1_macro 0.9974 0.7079
precision_macro 0.9975 0.7208
recall_macro 0.9973 0.7028
Test confusion matrix:
elapsed time : 0:00:03.568283
apport du preprocessing
On observe tout de suite l’apport des retraitemenst effectués à l’étape EDA : le modèle est passé à une performance de 70,79% sur le jeu de validation sans autres modifications
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_ | 0.669789 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
4.2.2.3. variante optimisée¶
Une autre variante consiste à essayer d’ajuster les hyper paramètres du pipeline dans l’espoire de gagner en performance. On reste sur le jeu de données prétraité
pipe = tfidf_RF_pipeline
params = target_params( pipe , {
"use_idf": [True, False],
"ngram_range": [(1, 1), (1, 2), (1,3)],
"bootstrap": [True, False],
"class_weight": ["balanced", None],
"n_estimators": [100, 300, 500],
})
base_TfIdf_RF_prepro_opti_= trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "base_TfIdf_RF_prepro_opti",
pipeline = pipe,
X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params( pipe , {'n_jobs':-1, 'random_state':42}),
use_opti = True,
iterable_params = params,
n_iter = 30
);
XP : base_TfIdf_RF_prepro_opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('coltext',
TextSelector(field='text')),
('tfidf',
TfidfVectorizer(use_idf=False)),
('classifier',
RandomForestClassifier(n_jobs=-1,
random_state=42))]),
n_iter=30, n_jobs=-1,
param_distributions={'classifier__bootstrap': [True, False],
'classifier__class_weight': ['balanced',
None],
'classifier__n_estimators': [100, 300,
500],
'tfidf__ngram_range': [(1, 1), (1, 2),
(1, 3)],
'tfidf__use_idf': [True, False]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': False, 'tfidf__ngram_range': (1, 1), 'classifier__n_estimators': 500, 'classifier__class_weight': 'balanced', 'classifier__bootstrap': False}
scores :
subset train test
metric
f1_macro 0.9974 0.7064
precision_macro 0.9972 0.7110
recall_macro 0.9975 0.7041
Test confusion matrix:
elapsed time : 0:55:30.032064
L’optimisation a eu ici un impact négatif très faible : f1 macro de 70,64% sur le jeu de validation, soit -0,15% , pour un temps de calcul démultiplié (55min vs 3sec)
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_ | 0.669789 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
4.2.2.4. Bag of Words avec régression logistique¶
Une autre variante : on essaie un autre classifier, la régression logistique

tfidf_LR_pipeline = Pipeline(
steps=[
('coltext', TextSelector('text')),
("tfidf", TfidfVectorizer()),
("classifier", LogisticRegression(solver='liblinear', multi_class='auto')),
]
)
list(bow_pipeline_LR.get_params().keys())
['memory',
'steps',
'verbose',
'coltext',
'tfidf',
'classifier',
'coltext__field',
'tfidf__analyzer',
'tfidf__binary',
'tfidf__decode_error',
'tfidf__dtype',
'tfidf__encoding',
'tfidf__input',
'tfidf__lowercase',
'tfidf__max_df',
'tfidf__max_features',
'tfidf__min_df',
'tfidf__ngram_range',
'tfidf__norm',
'tfidf__preprocessor',
'tfidf__smooth_idf',
'tfidf__stop_words',
'tfidf__strip_accents',
'tfidf__sublinear_tf',
'tfidf__token_pattern',
'tfidf__tokenizer',
'tfidf__use_idf',
'tfidf__vocabulary',
'classifier__C',
'classifier__class_weight',
'classifier__dual',
'classifier__fit_intercept',
'classifier__intercept_scaling',
'classifier__l1_ratio',
'classifier__max_iter',
'classifier__multi_class',
'classifier__n_jobs',
'classifier__penalty',
'classifier__random_state',
'classifier__solver',
'classifier__tol',
'classifier__verbose',
'classifier__warm_start']
pipe = tfidf_LR_pipeline
params = target_params( pipe , {
"use_idf": [True, False],
"ngram_range": [(1, 1), (1, 2), (1,3)],
"class_weight": [None, 'balanced']
})
TfIdf_LR_prepro_opti_ = trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "TfIdf_LR_prepro_opti",
pipeline = pipe,
X_train = X_train_prepro, y_train = y_train, X_test = X_val_prepro, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
use_opti = True,
iterable_params = params,
n_iter=30
);
XP : TfIdf_LR_prepro_opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('coltext',
TextSelector(field='text')),
('tfidf', TfidfVectorizer()),
('classifier',
LogisticRegression(n_jobs=-1,
random_state=42,
solver='liblinear'))]),
n_iter=30, n_jobs=-1,
param_distributions={'classifier__class_weight': [None,
'balanced'],
'tfidf__ngram_range': [(1, 1), (1, 2),
(1, 3)],
'tfidf__use_idf': [True, False]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': True, 'tfidf__ngram_range': (1, 1), 'classifier__class_weight': 'balanced'}
scores :
subset train test
metric
f1_macro 0.8057 0.6986
precision_macro 0.8097 0.7041
recall_macro 0.8027 0.6947
Test confusion matrix:
elapsed time : 0:00:07.844044
Le classifier LogisticRegression avec les données retraitées performe moins bien que le RandomForest (69,86% sur le jeu de validation).
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_ | 0.669789 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
Afin de vérifier si les données retraitées apprortent quelque chose on relance le même pipeline avec les jeux d’origine
pipe = tfidf_LR_pipeline
params = target_params( pipe , {
"use_idf": [True, False],
"ngram_range": [(1, 1), (1, 2), (1,3)],
"class_weight": [None, 'balanced']
})
TfIdf_LR_opti_ = trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "TfIdf_LR_opti",
pipeline = pipe,
X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
use_opti = True,
iterable_params = params,
n_iter=30
);
XP : TfIdf_LR_opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('coltext',
TextSelector(field='text')),
('tfidf', TfidfVectorizer()),
('classifier',
LogisticRegression(n_jobs=-1,
random_state=42,
solver='liblinear'))]),
n_iter=30, n_jobs=-1,
param_distributions={'classifier__class_weight': [None,
'balanced'],
'tfidf__ngram_range': [(1, 1), (1, 2),
(1, 3)],
'tfidf__use_idf': [True, False]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'tfidf__use_idf': True, 'tfidf__ngram_range': (1, 1), 'classifier__class_weight': 'balanced'}
scores :
subset train test
metric
f1_macro 0.8006 0.6999
precision_macro 0.8035 0.7031
recall_macro 0.7982 0.6973
Test confusion matrix:
elapsed time : 0:00:10.537456
On observe un comportement différent avec la régression logistice. Dans ce cas, c’est l’utilisation du jeu d’origine (avec le tokeniser par défaut de Tf Idf) qui apport de meilleurs résultats (69,99% sur le jeu de validation), sans toutefois égaler les performances du RandomForest
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_ | 0.669789 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
Ainsi les méthodes classiques nous aurons permi de gagner 3 points de f1 macro, le leader actuel étant le modèle RandomForest avec un simple BagOfWords (TfIdf=False) et optimisé dans ses paramètres sur le jeu de données prétraité.
| modèle | f1_macro_val | |
|---|---|---|
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
4.2.2.5. Optimisation du seuil de décision pour maximiser le f1¶
On peut aussi tirer avantage de la métrique utilisée pour l’évaluation. En effet, parmi les 3 catégories recherchées (negative, neutral et positive) il existe une gradation et en définitive, on est surtout intéressés à déterminer qsi un commentaire est positif ou négatif. La classification neutre étant une catégorie “par défaut” sans marqueur fort.
Stratégie : on maximise sur le jeu d’entraînement le seuil pour la décision positive, puis sur les non positifs, on maximise le seuil pour les négatifs, le reste est neutre
# permet de prendre une décision à partir d'un seuil
def to_labels(pos_probs, threshold):
return (pos_probs >= threshold).astype('int')
def find_optimal_f1_thresholds(pipe, X, y):
probs = pipe.predict_proba(X)
# On commence par travailler les prédictions positives
pos_probs = probs[:,2]
# On définit une échelle de seuils
thresholds = np.arange(0, 1, 0.001)
# On évalue le f1 pour chaque seuil
scores = [f1_score([(1 if i==1 else 0) for i in y ], to_labels(pos_probs, t)) for t in thresholds]
# On récupère le seuil optimal pour la catégorie positive
ix = np.argmax(scores)
res = {'pos_threshold' : thresholds[ix], 'pos_f1' : scores[ix] }
# On continue avec les prédictions négatives
neg_probs = probs[:,0]
# On définit une échelle de seuils
thresholds = np.arange(0, 1, 0.001)
# On évalue le f1 pour chaque seuil
scores = [f1_score([(1 if i==-1 else 0) for i in y ], to_labels(neg_probs, t)) for t in thresholds]
# On récupère le seuil optimal pour la catégorie positive
ix = np.argmax(scores)
res.update({'neg_threshold' : thresholds[ix], 'neg_f1' : scores[ix] })
return res
# startégie : on commence par décider si positif,
# sur les non positifs
def sentiment_predict(pipe, X, dict_thres):
'''
stratégie : on commence par décider si positif,
sur les non positifs, on décide si négatifs,
les restants sont neutres
'''
seuil_pos=dict_thres['pos_threshold']
seuil_neg=dict_thres['neg_threshold']
probs = pipe.predict_proba(X)
y_test_pred_pos = to_labels(probs[:,2], seuil_pos)
y_test_pred_neg = to_labels(probs[:,0], seuil_neg)
y_test_pred = y_test_pred_pos
y_test_pred[(y_test_pred_pos==0)] = -y_test_pred_neg[(y_test_pred_pos==0)]
return y_test_pred
thres = find_optimal_f1_thresholds(base_TfIdf_RF_prepro_opti_, X_train_prepro, y_train['sentiment'])
thres
{'pos_threshold': 0.39,
'pos_f1': 0.997985031663788,
'neg_threshold': 0.417,
'neg_f1': 0.9969359780680535}
y_val_pred = sentiment_predict(base_TfIdf_RF_prepro_opti_, X_val_prepro,thres)
f1_score(y_val, y_val_pred, average='macro')
0.7094767083062871
Le gain est modeste (+0,30%), mais reste dans les ordres de grandeur des optimisations de pipeline
| modèle | f1_macro_val | |
|---|---|---|
| 0 | TfIdf_LR_opti_modif_seuil | 0.709477 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
4.2.3. Approches par transformers pré entraînés¶
Le traiteùent du langage est un sujet notoirement complexe. Les approches classiques utilisées précédement s’appuyaient sur des approches fréquentistes (Tf Idf / Bag Of Words) et le retraitement manuel de certains aspects (URL, utilisateurs cités etc.).
Une méthode qui a fait ses preuves ces dernières années est l’utilisation du Deep Learning de manière générale et de l’architecture BERT en particulier.
Dans un mode de fonctionnement optimal, on devrait reprndre BERT et réentrainer la dernière couche uniquement pour le sujet de classification étudié. Pour des raisons de temps et de compétence, ce n’est pas l’approche prise ici.
Dans ce rapport, nous avons repris un modèle pré-entrainé dérivé de BERT et mis à disposition par HuggingFace
Plus précisement, le choix s’est porté sur le modèle roBERTa optimisé pour la tâche de classification de sentiment de Twitter
La difficulté principale rencontrée pour utiliser ce modèle a été d’adapter le fonctionnement du docker compose pour permettre l’accès aux ressources GPU du PC. Dans l’alternative, le temps de traitement était rédhibitoire.
4.2.3.1. Mise en place de l’environnement¶
import torch
torch.cuda.is_available()
True
from transformers import AutoModelForSequenceClassification
from transformers import TFAutoModelForSequenceClassification
from transformers import AutoTokenizer, AutoConfig
from transformers import pipeline
import numpy as np
from scipy.special import softmax
import csv
import urllib.request
# Preprocess text (username and link placeholders)
def preprocess(text):
new_text = []
for t in text.split(" "):
t = '@user' if t.startswith('@') and len(t) > 1 else t
t = 'http' if t.startswith('http') else t
new_text.append(t)
return " ".join(new_text)
Les modèles sont assez lourds (environ 500Mo)
Après avoir été téléchargé, il est important de réutiliser les documents sur disque
task='sentiment'
MODEL = f"cardiffnlp/twitter-roberta-base-{task}"
model = AutoModelForSequenceClassification.from_pretrained('/mnt/pretrained_models/'+MODEL)
tokenizer = AutoTokenizer.from_pretrained('/mnt/pretrained_models/'+MODEL)
config = AutoConfig.from_pretrained('/mnt/pretrained_models/'+MODEL)
# download label mapping
labels=[]
mapping_link = f"https://raw.githubusercontent.com/cardiffnlp/tweeteval/main/datasets/{task}/mapping.txt"
with urllib.request.urlopen(mapping_link) as f:
html = f.read().decode('utf-8').split("\n")
csvreader = csv.reader(html, delimiter='\t')
labels = [row[1] for row in csvreader if len(row) > 1]
nlp=pipeline("sentiment-analysis", model=model, tokenizer=tokenizer, device=0, return_all_scores=True)
def TorchTwitterRoBERTa_Pred(text = "Good night 😊"):
text = preprocess(text)
otpt = nlp(text)[0]
# otpt = (list(otpt[i].values())[1] for i in range(len(otpt)))
neg = otpt[0]['score']
neu = otpt[1]['score']
pos = otpt[2]['score']
# NewName = {0:'roBERTa-neg', 1:'roBERTa-neu', 2:'roBERTa-pos'}
# otpt = pd.json_normalize(otpt).transpose().rename(columns=NewName).reset_index().drop([0]).drop(columns=['index'])
return neg, neu, pos
test = TorchTwitterRoBERTa_Pred()
test
(0.007609867490828037, 0.1458120346069336, 0.8465781211853027)
La partie précédente permettait de transcrire le code de Huggingface.
Néanmoins l’utilisation pour faire des prédictions sur l’intégralité d’une base peut vite être longue. Le code suivant permet d’optimiser le temps de parcours des données.
def run_loopy_roBERTa(df):
v_neg, v_neu, v_pos = [], [], []
for _, row in df.iterrows():
v1, v2, v3 = TorchTwitterRoBERTa_Pred(row.values[0])
v_neg.append(v1)
v_neu.append(v2)
v_pos.append(v3)
df_result = pd.DataFrame({'roBERTa_neg': v_neg,
'roBERTa_neu': v_neu,
'roBERTa_pos': v_pos})
df_result.set_index(df.index)
return df_result
Afin d’utiliser la logique des pipelines, on crée une classe spécifique :
class clTwitterroBERTa(BaseEstimator, TransformerMixin):
def __init__(self, field):
self.field = field
def fit(self, X, y=None):
return self
def transform(self, X):
res = run_loopy_roBERTa(X[[self.field]])
return res
4.2.3.2. roBERTa Twitter Sentiment¶
On dispose désormais de tous les éléments nécessaires. roBERTa ayant été entraîné sur 58M de tweets en anglais, nous n’avons pas à appliquer de preprocessing en dehors de la standardisation des adresses et utilisateurs prévus par défaut dans le code de Huggingface.

roBERTa_pipe=Pipeline([
('roBERTa', clTwitterroBERTa(field='text'))
])
roBERTa_RF_Pipe = Pipeline(
steps=[
('roBERTa', roBERTa_pipe),
("classifier", RandomForestClassifier(n_jobs=-1))
]
)
pipe = roBERTa_RF_Pipe
roBERTa_RF_= trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter = "roBERTa_RF",
pipeline = pipe,
X_train = X_train, y_train = y_train, X_test = X_val, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42})
);
XP : roBERTa_RF
pipeline :
Pipeline(steps=[('roBERTa',
Pipeline(steps=[('roBERTa', clTwitterroBERTa(field='text'))])),
('classifier',
RandomForestClassifier(n_jobs=-1, random_state=42))])
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42}
scores :
subset train test
metric
f1_macro 1.0 0.7059
precision_macro 1.0 0.7049
recall_macro 1.0 0.7070
Test confusion matrix:
elapsed time : 0:05:54.986998
| modèle | f1_macro_val | |
|---|---|---|
| 0 | TfIdf_LR_opti_modif_seuil | 0.709477 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | roBERTa_RF_ | 0.705912 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
Sans optimisation, le modèle utilisant roBERTa tweet ne se place qu’en 4ème position, ce qui est en deçà des attentes a priori.
Par ailleurs, la quantité de mémoire vive à disposition sur la carte étant limitée, il n’a pas été possible d’effectuer une optimisation directe du pipeline, celle-ci créant des dépassements de mémoire.
C’est pourquoi la phase de prédiction par roBERTa tweet a été isolée (celle-ci ne présentant par ailleurs aps de possibilité de paramétrage) afin de laisser le seul classifier dans l’optimisation.
4.2.3.3. roBERTa Twitter Sentiment optimisé¶
X_train_roBERTa = roBERTa_pipe.transform(X_train)
X_val_roBERTa = roBERTa_pipe.transform(X_val)
X_test_roBERTa = roBERTa_pipe.transform(X_test)
X_train_roBERTa = X_train_roBERTa.set_index(X_train.index)
X_val_roBERTa = X_val_roBERTa.set_index(X_val.index)
X_test_roBERTa = X_test_roBERTa.set_index(X_test.index)
X_train_roBERTa
| roBERTa_neg | roBERTa_neu | roBERTa_pos | |
|---|---|---|---|
| 0 | 0.064939 | 0.808318 | 0.126744 |
| 1 | 0.918158 | 0.066100 | 0.015742 |
| 2 | 0.924613 | 0.070741 | 0.004646 |
| 3 | 0.783082 | 0.192980 | 0.023938 |
| 4 | 0.564197 | 0.404574 | 0.031229 |
| ... | ... | ... | ... |
| 21979 | 0.798430 | 0.183766 | 0.017804 |
| 21980 | 0.108279 | 0.705047 | 0.186674 |
| 21981 | 0.913698 | 0.076074 | 0.010228 |
| 21982 | 0.006624 | 0.053324 | 0.940052 |
| 21983 | 0.150456 | 0.322249 | 0.527295 |
21984 rows × 3 columns
X_train_roBERTa.to_parquet('/mnt/data/interim/X_train_roBERTa.gzip',compression='gzip')
X_val_roBERTa.to_parquet('/mnt/data/interim/X_val_roBERTa.gzip',compression='gzip')
X_test_roBERTa.to_parquet('/mnt/data/interim/X_test_roBERTa.gzip',compression='gzip')
X_train_roBERTa = pd.read_parquet('/mnt/data/interim/X_train_roBERTa.gzip')
X_val_roBERTa = pd.read_parquet('/mnt/data/interim/X_val_roBERTa.gzip')
X_test_roBERTa = pd.read_parquet('/mnt/data/interim/X_test_roBERTa.gzip')

roBERTa_RF = Pipeline(
steps=[
("classifier", RandomForestClassifier(n_jobs=-1))
]
)
pipe = roBERTa_RF
params = target_params(pipe, {
"bootstrap": [True, False],
"class_weight": ["balanced", None],
"n_estimators": [100, 300, 500, 800, 1200],
"max_depth": [5, 8, 15, 25, 30],
"min_samples_split": [2, 5, 10, 15, 100],
"min_samples_leaf": [1, 2, 5, 10]
})
roBERTa_RF_opti_ = trainPipelineMlFlow(
mlf_XP = "Rapport",
xp_name_iter="roBERTa_RF_opti",
pipeline = pipe,
X_train = X_train_roBERTa, y_train = y_train, X_test = X_val_roBERTa, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
use_opti = True,
iterable_params=params,
n_iter=30
);
XP : roBERTa_RF_opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('classifier',
RandomForestClassifier(n_jobs=-1,
random_state=42))]),
n_iter=30, n_jobs=-1,
param_distributions={'classifier__bootstrap': [True, False],
'classifier__class_weight': ['balanced',
None],
'classifier__max_depth': [5, 8, 15, 25,
30],
'classifier__min_samples_leaf': [1, 2,
5,
10],
'classifier__min_samples_split': [2, 5,
10,
15,
100],
'classifier__n_estimators': [100, 300,
500, 800,
1200]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 1200, 'classifier__min_samples_split': 100, 'classifier__min_samples_leaf': 5, 'classifier__max_depth': 8, 'classifier__class_weight': None, 'classifier__bootstrap': True}
scores :
subset train test
metric
f1_macro 0.7599 0.7466
precision_macro 0.7624 0.7488
recall_macro 0.7580 0.7450
Test confusion matrix:
elapsed time : 0:01:41.923590
Le modèle, une fois optimisé, arrive en haut du classement avec un gain de persque +4% de f1 pour atteindre 74,66% sur le jeu de validation
| modèle | f1_macro_val | |
|---|---|---|
| 0 | roBERTa_RF_opti_ | 0.746630 |
| 0 | TfIdf_LR_opti_modif_seuil | 0.709477 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | roBERTa_RF_ | 0.705912 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
4.2.3.4. Essai combinaison de différentes méthodes¶
Afin de gagner encore en performance, il est possible de combiner plusieurs outils d’estimation de sentimenst a priori. Ces transformations ne relevant pas des mêmes stratégies, elles capturent des éléments légèrement différents.
Les méthodes sélectionnées ici pour leur simplicité d’utilisation sont :

class Blob(BaseEstimator, TransformerMixin):
def __init__(self, field):
self.field = field
def fit(self, X, y=None):
return self
def transform(self, X):
X[['polarity', 'subjectivity']] = X[self.field].apply(lambda x:TextBlob(x).sentiment).apply(pd.Series)
return X[['polarity', 'subjectivity']]
blob_pipe=Pipeline([
('blob', Blob(field='text'))
])
X_train_Blob=blob_pipe.transform(X_train)
X_val_Blob=blob_pipe.transform(X_val)
X_test_Blob=blob_pipe.transform(X_test)
X_train_Blob.head()
| polarity | subjectivity | |
|---|---|---|
| 0 | 0.000000 | 0.0 |
| 1 | -0.976562 | 1.0 |
| 2 | 0.000000 | 0.0 |
| 3 | 0.000000 | 0.0 |
| 4 | 0.000000 | 0.0 |
X_train_Blob.to_parquet('/mnt/data/interim/X_train_Blob.gzip',compression='gzip')
X_val_Blob.to_parquet('/mnt/data/interim/X_val_Blob.gzip',compression='gzip')
X_test_Blob.to_parquet('/mnt/data/interim/X_test_Blob.gzip',compression='gzip')
X_train_Blob = pd.read_parquet('/mnt/data/interim/X_train_Blob.gzip')
X_val_Blob = pd.read_parquet('/mnt/data/interim/X_val_Blob.gzip')
X_test_Blob = pd.read_parquet('/mnt/data/interim/X_test_Blob.gzip')
On vérifie que TextBlob et roBERTa ne capturent pas les mêmes éléments.
TextBlob fournissant un indicateur global, on approxime les sentiments de rBERTa comme positive - negative
X =pd.DataFrame(columns=['roBERTa_sent'])
X['roBERTa_sent'] = X_train_roBERTa['roBERTa_pos']- X_train_roBERTa['roBERTa_neg']
X2 = pd.concat([X, X_train_Blob[['polarity']]], axis=1)
X2.corr()
| roBERTa_sent | polarity | |
|---|---|---|
| roBERTa_sent | 1.000000 | 0.563574 |
| polarity | 0.563574 | 1.000000 |
fig = px.scatter(x = X_train_roBERTa['roBERTa_pos']- X_train_roBERTa['roBERTa_neg'],
y = X_train_Blob['polarity'],
labels = {
'x': 'roBERTa',
'y' : 'TextBLob - polarity',
},
title = 'Comparaison des sentiments roBERTa vs TextBlob')
fig.show()
class Vader(BaseEstimator, TransformerMixin):
def __init__(self, field):
self.field = field
sid = SentimentIntensityAnalyzer()
def fit(self, X, y=None):
return self
def transform(self, X):
sid = SentimentIntensityAnalyzer()
X[['neg', 'neu', 'pos', 'compound']] = X[self.field].apply(sid.polarity_scores).apply(pd.Series)
return X[['neg', 'neu', 'pos', 'compound']]
vader_pipe=Pipeline([
('vader', Vader(field='text'))
])
X_train_Vader=vader_pipe.transform(X_train)
X_val_Vader=vader_pipe.transform(X_val)
X_test_Vader=vader_pipe.transform(X_test)
X_train_Vader.head()
| neg | neu | pos | compound | |
|---|---|---|---|---|
| 0 | 0.000 | 1.000 | 0.0 | 0.0000 |
| 1 | 0.474 | 0.526 | 0.0 | -0.7437 |
| 2 | 0.494 | 0.506 | 0.0 | -0.5994 |
| 3 | 0.538 | 0.462 | 0.0 | -0.3595 |
| 4 | 0.000 | 1.000 | 0.0 | 0.0000 |
X_train_Vader.to_parquet('/mnt/data/interim/X_train_Vader.gzip',compression='gzip')
X_val_Vader.to_parquet('/mnt/data/interim/X_val_Vader.gzip',compression='gzip')
X_test_Vader.to_parquet('/mnt/data/interim/X_test_Vader.gzip',compression='gzip')
X_train_Vader = pd.read_parquet('/mnt/data/interim/X_train_Vader.gzip')
X_val_Vader = pd.read_parquet('/mnt/data/interim/X_val_Vader.gzip')
X_test_Vader = pd.read_parquet('/mnt/data/interim/X_test_Vader.gzip')
On vérifie de la même manière que Vader et roBERTa ne capturent pas les mêmes éléments.
Pour les positifs
X_pos = pd.concat([X_train_roBERTa[['roBERTa_pos']], X_train_Vader[['pos']]], axis=1)
X_pos.corr()
| roBERTa_pos | pos | |
|---|---|---|
| roBERTa_pos | 1.000000 | 0.607805 |
| pos | 0.607805 | 1.000000 |
fig = px.scatter(x = X_train_roBERTa['roBERTa_pos'],
y = X_train_Vader['pos'],
labels = {
'x': 'roBERTa',
'y' : 'Vader',
},
title = 'Comparaison des sentiments roBERTa vs Vaders - positive')
fig.show()
Pour les neutres
X_pos = pd.concat([X_train_roBERTa[['roBERTa_neu']], X_train_Vader[['neu']]], axis=1)
X_pos.corr()
| roBERTa_neu | neu | |
|---|---|---|
| roBERTa_neu | 1.000000 | 0.473356 |
| neu | 0.473356 | 1.000000 |
fig = px.scatter(x = X_train_roBERTa['roBERTa_neu'],
y = X_train_Vader['neu'],
labels = {
'x': 'roBERTa',
'y' : 'Vader',
},
title = 'Comparaison des sentiments roBERTa vs Vaders - neutral')
fig.show()
Pour les négatifs
X_pos = pd.concat([X_train_roBERTa[['roBERTa_neg']], X_train_Vader[['neg']]], axis=1)
X_pos.corr()
| roBERTa_neg | neg | |
|---|---|---|
| roBERTa_neg | 1.000000 | 0.560029 |
| neg | 0.560029 | 1.000000 |
fig = px.scatter(x = X_train_roBERTa['roBERTa_neg'],
y = X_train_Vader['neg'],
labels = {
'x': 'roBERTa',
'y' : 'Vader',
},
title = 'Comparaison des sentiments roBERTa vs Vaders - negative')
fig.show()
On peut alors calculer la base agrégée
X_train_compound = pd.concat([X_train_roBERTa, X_train_Blob, X_train_Vader], axis=1)
X_val_compound = pd.concat([X_val_roBERTa, X_val_Blob, X_val_Vader], axis=1)
X_test_compound = pd.concat([X_test_roBERTa, X_test_Blob, X_test_Vader], axis=1)
X_train_compound.head()
| roBERTa_neg | roBERTa_neu | roBERTa_pos | polarity | subjectivity | neg | neu | pos | compound | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.064939 | 0.808318 | 0.126744 | 0.000000 | 0.0 | 0.000 | 1.000 | 0.0 | 0.0000 |
| 1 | 0.918158 | 0.066100 | 0.015742 | -0.976562 | 1.0 | 0.474 | 0.526 | 0.0 | -0.7437 |
| 2 | 0.924613 | 0.070741 | 0.004646 | 0.000000 | 0.0 | 0.494 | 0.506 | 0.0 | -0.5994 |
| 3 | 0.783082 | 0.192980 | 0.023938 | 0.000000 | 0.0 | 0.538 | 0.462 | 0.0 | -0.3595 |
| 4 | 0.564197 | 0.404574 | 0.031229 | 0.000000 | 0.0 | 0.000 | 1.000 | 0.0 | 0.0000 |
X_val_compound.head()
| roBERTa_neg | roBERTa_neu | roBERTa_pos | polarity | subjectivity | neg | neu | pos | compound | |
|---|---|---|---|---|---|---|---|---|---|
| 21984 | 0.562985 | 0.367200 | 0.069815 | 0.0 | 0.0000 | 0.211 | 0.789 | 0.000 | -0.1531 |
| 21985 | 0.226580 | 0.667382 | 0.106038 | 0.0 | 0.0000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 21986 | 0.002332 | 0.476234 | 0.521434 | 0.0 | 0.0000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 21987 | 0.296688 | 0.569384 | 0.133927 | 0.0 | 0.0000 | 0.000 | 0.799 | 0.201 | 0.7003 |
| 21988 | 0.004010 | 0.177016 | 0.818974 | 0.4 | 0.5125 | 0.000 | 0.864 | 0.136 | 0.4588 |
Tip
Comme on travaille avec des arbres il n’y a pas besoin de renormer / standardiser les différentes colonnes
pipe = roBERTa_RF
params = target_params(pipe, {
"bootstrap": [True, False],
"class_weight": ["balanced", None],
"n_estimators": [100, 300, 500, 800, 1200],
"max_depth": [5, 8, 15, 25, 30],
"min_samples_split": [2, 5, 10, 15, 100],
"min_samples_leaf": [1, 2, 5, 10]
})
roBERTa_Blob_Vader_RF_opti_ = trainPipelineMlFlow(
mlf_XP="Rapport",
xp_name_iter="roBERTa_Blob_Vader_RF_opti",
pipeline = pipe,
X_train = X_train_compound, y_train = y_train, X_test = X_val_compound, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42}),
use_opti = True,
iterable_params = params,
n_iter = 30
);
XP : roBERTa_Blob_Vader_RF_opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('classifier',
RandomForestClassifier(n_jobs=-1,
random_state=42))]),
n_iter=30, n_jobs=-1,
param_distributions={'classifier__bootstrap': [True, False],
'classifier__class_weight': ['balanced',
None],
'classifier__max_depth': [5, 8, 15, 25,
30],
'classifier__min_samples_leaf': [1, 2,
5,
10],
'classifier__min_samples_split': [2, 5,
10,
15,
100],
'classifier__n_estimators': [100, 300,
500, 800,
1200]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 500, 'classifier__min_samples_split': 15, 'classifier__min_samples_leaf': 10, 'classifier__max_depth': 15, 'classifier__class_weight': None, 'classifier__bootstrap': True}
scores :
subset train test
metric
f1_macro 0.8301 0.7567
precision_macro 0.8333 0.7591
recall_macro 0.8276 0.7546
Test confusion matrix:
elapsed time : 0:02:30.393318
| modèle | f1_macro_val | |
|---|---|---|
| 0 | roBERTa_Blob_Vader_RF_opti_ | 0.756699 |
| 0 | roBERTa_RF_opti_ | 0.746630 |
| 0 | TfIdf_LR_opti_modif_seuil | 0.709477 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | roBERTa_RF_ | 0.705912 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
Note
L’utilisation de Vader et Blob en soutien de roBERTa a permi de gagner 1 point de f1 sur le jeu de validation
4.2.4. Essai xgboost sur combinaison de méthodes¶
Dans ce dernier essai on remplace le RandomClassifier par un XGBoost

import xgboost as xgb
roBERTa_xgb = Pipeline(
steps=[
("classifier", xgb.XGBClassifier())
]
)
Tip
Dans l’exemple ci-dessous on utilise explicitement le GPU pour accélérer les calculs (tree_method = 'gpu_hist' et gpu_id=0) .
Le gain est saisissant : dans une première version qui n’utilisait que le CPU, le modèle tournait en 3h30, contre un peu plus de 4 min ici, soit un gain de temps de presque 1:52!
pipe = roBERTa_xgb
params = target_params(pipe, {
"eta" : [0.05, 0.10, 0.15, 0.20, 0.25, 0.30 ] ,
"max_depth" : [ 3, 4, 5, 6, 8, 10, 12, 15],
"min_child_weight" : [ 1, 3, 5, 7 ],
"gamma" : [ 0.0, 0.1, 0.2 , 0.3, 0.4 ],
"colsample_bytree" : [ 0.3, 0.4, 0.5 , 0.7 ]
})
roBERTa_xgb_opti_ = trainPipelineMlFlow(
mlf_XP="DSA_Tweets",
xp_name_iter="roBERTa - xgb - opti",
pipeline = pipe,
X_train = X_train_compound, y_train = y_train, X_test = X_val_compound, y_test = y_val,
target_col = 'sentiment',
fixed_params = target_params(pipe, {'n_jobs':-1,'random_state':42, 'gpu_id':0, 'tree_method' : 'gpu_hist'}),
use_opti = True,
iterable_params=params,
n_iter=20
)
[03:37:26] WARNING: ../src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'multi:softprob' was changed from 'merror' to 'mlogloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
XP : roBERTa - xgb - opti
pipeline :
RandomizedSearchCV(cv=5,
estimator=Pipeline(steps=[('classifier',
XGBClassifier(base_score=None,
booster=None,
colsample_bylevel=None,
colsample_bynode=None,
colsample_bytree=None,
gamma=None,
gpu_id=0,
importance_type='gain',
interaction_constraints=None,
learning_rate=None,
max_delta_step=None,
max_depth=None,
min_child_weight=None,
missing=nan,
monotone_constra...
scale_pos_weight=None,
subsample=None,
tree_method='gpu_hist',
validate_parameters=None,
verbosity=None))]),
n_iter=20, n_jobs=-1,
param_distributions={'classifier__colsample_bytree': [0.3,
0.4,
0.5,
0.7],
'classifier__gamma': [0.0, 0.1, 0.2,
0.3, 0.4],
'classifier__max_depth': [3, 4, 5, 6, 8,
10, 12, 15],
'classifier__min_child_weight': [1, 3,
5,
7]},
random_state=42, scoring='f1_macro')
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__gpu_id': 0, 'classifier__tree_method': 'gpu_hist', 'classifier__min_child_weight': 7, 'classifier__max_depth': 5, 'classifier__gamma': 0.1, 'classifier__colsample_bytree': 0.7}
scores :
subset train test
metric
f1_macro 0.8196 0.7591
precision_macro 0.8223 0.7614
recall_macro 0.8174 0.7572
Test confusion matrix:
elapsed time : 0:04:54.512383
On essaye de comprendre les 27 tweets négatifs qui ont été prédits positifs
Le modèle XGBoost optimisé à partir des données augmenté se hisse à la première place du podium
| modèle | f1_macro_val | |
|---|---|---|
| 0 | roBERTa_xgb_opti_ | 0.759147 |
| 0 | roBERTa_Blob_Vader_RF_opti_ | 0.756699 |
| 0 | roBERTa_RF_opti_ | 0.746630 |
| 0 | TfIdf_LR_opti_modif_seuil | 0.709477 |
| 0 | base_TfIdf_RF_prepro_ | 0.707919 |
| 0 | base_TfIdf_RF_prepro_opti_ | 0.706432 |
| 0 | roBERTa_RF_ | 0.705912 |
| 0 | TfIdf_LR_opti_ | 0.699877 |
| 0 | TfIdf_LR_prepro_opti_ | 0.698565 |
| 0 | base_TfIdf_RF_ | 0.669789 |
On peut s’interroger sur les prédiction restantes qui sont positives, mais classées comme négatives et inversement
y_val_pred = roBERTa_xgb_opti_.predict(X_val_compound)
inpt = pd.concat([X_val, X_val_compound], axis=1)
exemples_realNeg_predPos_fin = inpt[(y_val['sentiment']==-1) & (y_val_pred==1)]
exemples_realPos_predNeg_fin = inpt[(y_val['sentiment']==1) & (y_val_pred==-1)]
exemples_realNeg_predPos_fin
| text | roBERTa_neg | roBERTa_neu | roBERTa_pos | polarity | subjectivity | neg | neu | pos | compound | |
|---|---|---|---|---|---|---|---|---|---|---|
| 22095 | Omg I want TF2, everybody on my Steam Friends list is playing it | 0.001319 | 0.039033 | 0.959648 | 0.000000 | 0.000000 | 0.000 | 0.592 | 0.408 | 0.6369 |
| 22222 | Hi, my name is Kate and I`m addicted to mm`s! | 0.009058 | 0.103601 | 0.887340 | -0.500000 | 0.600000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 22424 | I miss my dog r.i.p.Batman... Yeah, Batman (I really hope `all dogs go to heaven` is true) | 0.073255 | 0.355204 | 0.571541 | 0.275000 | 0.425000 | 0.080 | 0.650 | 0.270 | 0.5849 |
| 22498 | My storm is acting up ....Excited for the discussion session regarding Social Media. Scott Lake, CEO of ThinkSM, will be attending. | 0.000800 | 0.013065 | 0.986135 | 0.016667 | 0.033333 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 23387 | it`s ridiculously warm in bed | 0.003796 | 0.075537 | 0.920667 | 0.600000 | 0.600000 | 0.329 | 0.411 | 0.260 | -0.1280 |
| 23874 | i want to see my bud mel miss hur loads | 0.084246 | 0.431542 | 0.484211 | 0.000000 | 0.000000 | 0.162 | 0.707 | 0.131 | -0.0772 |
| 23932 | and this is better 4 me than a snickers bar or hersheys w/almonds.... which i use to b addicted to b4. i miss them LOL | 0.029847 | 0.111354 | 0.858799 | 0.300000 | 0.600000 | 0.064 | 0.679 | 0.257 | 0.7034 |
| 24100 | AHHHHHHH!!!!! its my 16th birthday And i cant belive i found out im seeing yous tonight Best present everr!!! <333333333 | 0.001637 | 0.006869 | 0.991494 | 0.500000 | 0.150000 | 0.000 | 0.760 | 0.240 | 0.7482 |
| 24443 | Hav fun at heav y Metal happy hour you guys! In the future accadentally sets it on fire while smoking with | 0.002921 | 0.033595 | 0.963485 | 0.433333 | 0.441667 | 0.090 | 0.637 | 0.273 | 0.7088 |
| 24468 | So glad the days almost over... Another nite of me nd my pain pills alone at the crib lol. Ughh I wish this weekend was over alreadi! | 0.038555 | 0.112199 | 0.849246 | 0.750000 | 0.850000 | 0.150 | 0.593 | 0.257 | 0.5838 |
| 24789 | where`s enthusiasm in meeeee | 0.332726 | 0.539401 | 0.127872 | 0.000000 | 0.000000 | 0.000 | 0.508 | 0.492 | 0.4404 |
| 24850 | Good work.I`ve only just managed to turn my studio on... I envy your productivity | 0.008150 | 0.049628 | 0.942222 | 0.350000 | 0.800000 | 0.131 | 0.688 | 0.181 | 0.2023 |
| 25027 | I`m feeling much less alone now in my love for Fitzcarraldo, most people I mention it to have no idea what I am talking about. | 0.060577 | 0.296912 | 0.642512 | 0.277778 | 0.388889 | 0.137 | 0.664 | 0.199 | 0.4201 |
| 25245 | () Gonna watch JT on SNL tonight - not a fan of his music but think he`s hilarious! `**** in my Pants` - WAY too funny | 0.043971 | 0.154075 | 0.801954 | 0.437500 | 1.000000 | 0.049 | 0.634 | 0.317 | 0.8413 |
| 25911 | Ate too much vegetarian pizza for dinner! But it was so good | 0.002520 | 0.014339 | 0.983142 | 0.475000 | 0.400000 | 0.000 | 0.671 | 0.329 | 0.7509 |
| 25923 | - God i`m up early. Hayley still asleep but today is party day so i`m getting stuff ready. x | 0.016226 | 0.263445 | 0.720329 | 0.150000 | 0.400000 | 0.000 | 0.626 | 0.374 | 0.8100 |
| 26321 | TIRED! goodnight twitter its mother`s day happy mother`s day lov my moomy <3 yayy! God Bless. | 0.002120 | 0.015923 | 0.981957 | 0.433333 | 0.900000 | 0.136 | 0.412 | 0.452 | 0.8152 |
| 26328 | Oh great Tampa people - anybody in the area know someone who works for the Jain Society of Tampa Bay? None of their phone #`s work. | 0.290720 | 0.496840 | 0.212440 | 0.800000 | 0.750000 | 0.000 | 0.854 | 0.146 | 0.6249 |
| 26464 | Steve makes fruit smoothies for me each day & they are berry delicious, I made mine today & it was berry, berry bad | 0.011197 | 0.097974 | 0.890829 | 0.150000 | 0.833333 | 0.139 | 0.714 | 0.147 | 0.0516 |
| 26529 | They have a list of 50 state parks here in PA that are under consideration for closing. Nice ones too. | 0.018955 | 0.241769 | 0.739276 | 0.600000 | 1.000000 | 0.000 | 0.865 | 0.135 | 0.4215 |
| 26561 | I would totally take you to prom...if i hadnt already gone sorry lol | 0.057130 | 0.328177 | 0.614693 | 0.100000 | 0.816667 | 0.000 | 0.691 | 0.309 | 0.4628 |
| 26576 | its too sunny for work !!! | 0.108557 | 0.347588 | 0.543854 | 0.000000 | 0.000000 | 0.000 | 0.576 | 0.424 | 0.5684 |
| 26740 | The 22nd can`t get here fast enough! | 0.008742 | 0.081710 | 0.909548 | 0.100000 | 0.550000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 26900 | Happy Mother`s Day to every mommy out there | 0.001472 | 0.015549 | 0.982979 | 0.800000 | 1.000000 | 0.000 | 0.654 | 0.346 | 0.5719 |
| 27060 | Bruno arghhhh i cant wait | 0.004612 | 0.025054 | 0.970334 | 0.000000 | 0.000000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 27164 | Obviously not too bad | 0.074839 | 0.330566 | 0.594595 | -0.350000 | 0.583333 | 0.000 | 0.513 | 0.487 | 0.4310 |
| 27219 | I can`t believe you tweeted that. It was our special moment | 0.104980 | 0.375221 | 0.519799 | 0.357143 | 0.571429 | 0.000 | 0.769 | 0.231 | 0.4019 |
Contrairement à la première analyse, on observe que le taux de tweets vraisemblablement mal libellés semble plus important
exemples_realPos_predNeg_fin
| text | roBERTa_neg | roBERTa_neu | roBERTa_pos | polarity | subjectivity | neg | neu | pos | compound | |
|---|---|---|---|---|---|---|---|---|---|---|
| 22162 | I wish I can see that. They have CNN here again, with no volume. | 0.634638 | 0.326405 | 0.038957 | 0.000000 | 0.000000 | 0.148 | 0.671 | 0.181 | 0.1280 |
| 22281 | _22 ok so I`m having a complete insomniac moment. It`s 6am(almost) and I`m STILL awake. I hate when I can`t stop thinking! mornin! | 0.929382 | 0.059761 | 0.010857 | -0.133333 | 0.600000 | 0.252 | 0.662 | 0.086 | -0.6467 |
| 22404 | i hope my morning show doesn`t get cancelled! | 0.548575 | 0.370015 | 0.081410 | 0.000000 | 0.000000 | 0.196 | 0.491 | 0.313 | 0.2942 |
| 22630 | let me guess ... ran a few miles? Respect dude, I can`t do it. Maybe you should train me | 0.712335 | 0.261885 | 0.025779 | -0.200000 | 0.100000 | 0.000 | 0.838 | 0.162 | 0.4767 |
| 22807 | Why don`t we close the library due to the great weather? And the ac isn`t working #fb | 0.701326 | 0.255291 | 0.043382 | 0.337500 | 0.562500 | 0.000 | 0.796 | 0.204 | 0.6249 |
| 22817 | Why do you hurt me? Does it bring you joy to see me cry? You know I love you more then anything and yet u break my heart everyday! | 0.929708 | 0.057478 | 0.012813 | 0.387500 | 0.475000 | 0.170 | 0.603 | 0.227 | 0.4857 |
| 23044 | Big Laptop is too big, so it`s time to switch to the Eee. Bye big guy | 0.590308 | 0.326236 | 0.083455 | 0.000000 | 0.100000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 23281 | Aww just read your tweet. I`m not sure about later either (work too) feel it for us | 0.577661 | 0.382728 | 0.039611 | 0.016667 | 0.596296 | 0.109 | 0.891 | 0.000 | -0.2411 |
| 23308 | My palms are itchy. Doesn`t that mean something about coming into a great deal of money? | 0.598176 | 0.352266 | 0.049558 | 0.243750 | 0.718750 | 0.109 | 0.677 | 0.214 | 0.4588 |
| 23402 | Good morning I don`t think it has stopped raining once for the past three days or so, but who cares? | 0.287069 | 0.541327 | 0.171604 | 0.225000 | 0.425000 | 0.061 | 0.672 | 0.267 | 0.7092 |
| 23536 | I gave up cable in these tough economic times. it was either cable or shoes, and you know what cable lost | 0.776729 | 0.203081 | 0.020190 | -0.094444 | 0.516667 | 0.174 | 0.826 | 0.000 | -0.4215 |
| 23610 | _mcc bye, Scotty! i`m gonna miss you. ily<333 | 0.093740 | 0.366023 | 0.540237 | 0.000000 | 0.000000 | 0.213 | 0.787 | 0.000 | -0.2244 |
| 23784 | Funtime was not a lot of fun!! But finally done with it | 0.835564 | 0.122180 | 0.042256 | 0.234375 | 0.600000 | 0.196 | 0.804 | 0.000 | -0.3474 |
| 23864 | on that note - i do not feel missed. | 0.601743 | 0.335804 | 0.062453 | 0.000000 | 0.000000 | 0.000 | 0.761 | 0.239 | 0.2235 |
| 24016 | morning world, is raining 2day so revision don`t seem so tough, | 0.039120 | 0.401251 | 0.559628 | -0.388889 | 0.833333 | 0.166 | 0.834 | 0.000 | -0.2479 |
| 24305 | ._.; Thanxxx ! Now with that message I just wanna leave !! )= ! BYE ! | 0.682855 | 0.271553 | 0.045592 | 0.000000 | 0.000000 | 0.177 | 0.823 | 0.000 | -0.3331 |
| 24435 | IIII know!!! and mean | 0.598222 | 0.349651 | 0.052128 | -0.312500 | 0.687500 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 24623 | whooaaa. just got an overwheolming itus attack after eating | 0.833472 | 0.154779 | 0.011749 | 0.000000 | 0.000000 | 0.279 | 0.721 | 0.000 | -0.4767 |
| 24628 | Is dreading going to work BUT....its friiiiiday!! whoop!!! | 0.593392 | 0.300574 | 0.106034 | 0.000000 | 0.000000 | 0.395 | 0.605 | 0.000 | -0.6776 |
| 24740 | dude... Can you really be a bachelor at this point?? Don`t worry about it. | 0.456769 | 0.477203 | 0.066028 | 0.200000 | 0.200000 | 0.214 | 0.786 | 0.000 | -0.5040 |
| 24757 | No. That would be too easy. All I have is the user manual which is not enough for me to claim his bike | 0.609687 | 0.338870 | 0.051443 | 0.216667 | 0.666667 | 0.088 | 0.797 | 0.116 | 0.1779 |
| 24848 | i think i`ll be home more than i want to be next week - no work booked in for the forseeable. | 0.040266 | 0.400941 | 0.558794 | 0.250000 | 0.250000 | 0.111 | 0.809 | 0.080 | -0.1585 |
| 24928 | two macaroons go into a bar....one says oh your a nut. wow I need to get out more. | 0.675843 | 0.256036 | 0.068120 | 0.300000 | 0.750000 | 0.000 | 0.787 | 0.213 | 0.5859 |
| 25016 | While on vacation, having golden times spamming Google by 5.350 redirects. http://bit.ly/18kwzh | 0.308829 | 0.552101 | 0.139071 | 0.300000 | 0.500000 | 0.220 | 0.780 | 0.000 | -0.4767 |
| 25061 | i want so bad to go to the mcfly`s concert anybody up to go with me? | 0.310271 | 0.435570 | 0.254159 | -0.700000 | 0.666667 | 0.210 | 0.719 | 0.072 | -0.5413 |
| 25195 | they are all over one is a fan with a vip and the other one is the winner of the twisted vid het si weer eens raar gelopen, chaos | 0.392492 | 0.533716 | 0.073792 | -0.312500 | 0.687500 | 0.102 | 0.637 | 0.260 | 0.6908 |
| 25335 | thinking about new.. oh yes .. btw bankroll stays at $14.88.. so down a bit from yesterday.. and I won`t whine about bad beats .. | 0.046912 | 0.496715 | 0.456373 | -0.239731 | 0.470034 | 0.209 | 0.697 | 0.094 | -0.5106 |
| 25340 | Oh no! I hope you find your kitten | 0.616610 | 0.311418 | 0.071972 | 0.000000 | 0.000000 | 0.212 | 0.481 | 0.307 | 0.2481 |
| 25398 | There was no traffic at all on my way home and all traffic lights were green.Im afraidIowe karma a big check | 0.331551 | 0.444313 | 0.224136 | 0.000000 | 0.100000 | 0.104 | 0.896 | 0.000 | -0.2960 |
| 25788 | _Jo my mask is non-existent at the mo Charis didn`t send me one & I haven`t been bothered to make one! I`m wearing boy clothes! | 0.653728 | 0.305131 | 0.041140 | 0.000000 | 0.000000 | 0.116 | 0.884 | 0.000 | -0.4374 |
| 25807 | http://twitpic.com/4hbs5 - ahahahahahahahaha can i please eat that off your head **** | 0.804986 | 0.171911 | 0.023103 | 0.000000 | 0.000000 | 0.000 | 0.796 | 0.204 | 0.3182 |
| 26008 | i really wish i could make it! a 12 hr. drive just isn`t going to happen this weekend. | 0.633561 | 0.278867 | 0.087571 | 0.250000 | 0.200000 | 0.000 | 0.810 | 0.190 | 0.5081 |
| 26191 | Full, thanks for the food Jean I should have brought that half of the watermelon with me and eat it on the freeway and crash and die. | 0.362044 | 0.414240 | 0.223716 | 0.127778 | 0.305556 | 0.202 | 0.673 | 0.125 | -0.5423 |
| 26230 | awwww this made me realize I have to take down my bulletin board too! There`s so many memories up there. | 0.585399 | 0.332485 | 0.082117 | 0.268519 | 0.562963 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 26628 | _eyes I wish I knew! The curse of Tumblr. | 0.696642 | 0.250489 | 0.052870 | 0.000000 | 0.000000 | 0.330 | 0.435 | 0.235 | -0.2714 |
| 26662 | ty for feeding our NK addiction..ermm i mean our uhh nope yup addiction covers it | 0.399613 | 0.436190 | 0.164197 | -0.312500 | 0.687500 | 0.000 | 0.833 | 0.167 | 0.3818 |
| 26665 | I`m so bunged up!! I Hate colds!! | 0.963905 | 0.029016 | 0.007078 | -1.000000 | 0.900000 | 0.506 | 0.494 | 0.000 | -0.7296 |
| 26673 | I`ve burnt my collerbone and arms and face! aww! | 0.941750 | 0.050507 | 0.007743 | 0.375000 | 0.900000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 26720 | I know, right?!? I have such a lead foot | 0.651567 | 0.300968 | 0.047465 | 0.178571 | 0.517857 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 26780 | **left off the 'again' in the title...whoops! | 0.606299 | 0.344978 | 0.048722 | 0.000000 | 0.000000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 26787 | i`m gonna miss all the live Comet action tomorrow! i have to go take care of my cousins and they don`t have access to the interwebz | 0.834206 | 0.148361 | 0.017433 | 0.130682 | 0.300000 | 0.057 | 0.819 | 0.124 | 0.4389 |
| 26872 | Agreed! Though Eclipse apps hinder collecting the heap dump by catching OOME. Had to muck about in JConsole | 0.748078 | 0.222225 | 0.029696 | 0.600000 | 0.900000 | 0.138 | 0.762 | 0.100 | -0.2003 |
| 26879 | _Khan oh please, you don`t have to do that to me. Don`t bother | 0.764604 | 0.222715 | 0.012681 | 0.000000 | 0.000000 | 0.153 | 0.701 | 0.146 | -0.0258 |
| 27042 | Urghh, I`m gonna do my project now don`t wanna waste valuable weekend time | 0.603666 | 0.331370 | 0.064964 | -0.200000 | 0.000000 | 0.166 | 0.651 | 0.183 | 0.0772 |
| 27063 | I was looking forward to seeing in Raleigh (fan for 10 years NB too) but scalpers took the tix and sell them for $200 morons | 0.713850 | 0.240869 | 0.045281 | -0.800000 | 1.000000 | 0.114 | 0.886 | 0.000 | -0.4497 |
| 27180 | Oh no I hope you reach him! | 0.466357 | 0.410455 | 0.123188 | 0.000000 | 0.000000 | 0.232 | 0.316 | 0.452 | 0.2714 |
| 27190 | friday night is my fav night of the week but now I have to go to stupid dog training classes | 0.837420 | 0.134528 | 0.028051 | -0.800000 | 1.000000 | 0.195 | 0.720 | 0.085 | -0.5574 |
| 27210 | Ooooh! I thought eggos were some kind of synthetic egg or egg sustitute or something. You crazy Americans | 0.903786 | 0.086572 | 0.009642 | 0.000000 | 0.900000 | 0.144 | 0.856 | 0.000 | -0.4003 |
| 27370 | i have a crush on someone! | 0.231675 | 0.588623 | 0.179702 | 0.000000 | 0.000000 | 0.387 | 0.613 | 0.000 | -0.2244 |
| 27427 | i hate my presentation hahah whatever im glad its over | 0.705794 | 0.207846 | 0.086359 | -0.150000 | 0.950000 | 0.270 | 0.511 | 0.219 | -0.1779 |
| 27457 | I really wish someone would make a groupchat theme for Adium suited for IRC. yMous has way too low contrast. | 0.686272 | 0.264261 | 0.049467 | 0.100000 | 0.250000 | 0.100 | 0.759 | 0.142 | 0.2247 |
On peut essayer de comprendre comment le modèle exploite les informations fournies pour prendre ses décisions en s’appuyant sur SHAP
#set the tree explainer as the model of the pipeline
explainer = shap.TreeExplainer(roBERTa_xgb_opti_.best_estimator_['classifier'])
#apply the preprocessing to x_test
#observations = pipeline['imputer'].transform(x_test)
observations = X_val_compound
#get Shap values from preprocessed data
shap_values = explainer.shap_values(observations)
#plot the feature importance
titres = {0 : 'Prédictions négatives', 1: 'Prédictions neutres', 2 : 'Prédictions positives'}
for i in range(3):
shap.summary_plot(shap_values[i], observations, plot_type="bar", show=False)
plt.title(titres[i])
plt.show()
Tip
Cette première analyse confirme que le modèle s’appuie principalement sur les prédiction de roBERTa tweet de la classification idoine pour prendre sa décision. Les autres composantes ayant des contributions bien plus faibles.
On peut ensuite zoomer sur la manière dont les prédictions de roBERTa sont prises en compte dans le modèle :
titres = {0 : 'Prédictions négatives', 1: 'Prédictions neutres', 2 : 'Prédictions positives'}
axe = {0 : 'roBERTa_neg', 1: 'roBERTa_neu', 2 : 'roBERTa_pos'}
for i in range(3):
shap.dependence_plot(axe[i], shap_values[i], X_val_compound, show=False)
plt.title(titres[i])
plt.show()
4.3. Soumission finale¶
On réentraine les 2 modèles finalistes sur le jeu de validation sur l’intégralité de tain + val et on évalue sur le jeu de test
Warning
Ici on réajuste uniquement les modèles sans explorer de nouvelles vvaleur d’hyperparamètres
y_train_tot = pd.concat([y_train, y_val], axis=0)
X_train_compound_tot = pd.concat([X_train_compound, X_val_compound], axis=0)
X_train_compound_tot
| roBERTa_neg | roBERTa_neu | roBERTa_pos | polarity | subjectivity | neg | neu | pos | compound | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.064939 | 0.808318 | 0.126744 | 0.000000 | 0.000000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| 1 | 0.918158 | 0.066100 | 0.015742 | -0.976562 | 1.000000 | 0.474 | 0.526 | 0.000 | -0.7437 |
| 2 | 0.924613 | 0.070741 | 0.004646 | 0.000000 | 0.000000 | 0.494 | 0.506 | 0.000 | -0.5994 |
| 3 | 0.783082 | 0.192980 | 0.023938 | 0.000000 | 0.000000 | 0.538 | 0.462 | 0.000 | -0.3595 |
| 4 | 0.564197 | 0.404574 | 0.031229 | 0.000000 | 0.000000 | 0.000 | 1.000 | 0.000 | 0.0000 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 27475 | 0.434403 | 0.445122 | 0.120474 | 0.000000 | 0.000000 | 0.128 | 0.722 | 0.150 | 0.1027 |
| 27476 | 0.139542 | 0.635024 | 0.225433 | 0.184091 | 0.646970 | 0.000 | 0.890 | 0.110 | 0.3818 |
| 27477 | 0.003337 | 0.022629 | 0.974034 | 0.366667 | 0.533333 | 0.000 | 0.572 | 0.428 | 0.9136 |
| 27478 | 0.053331 | 0.357756 | 0.588913 | 0.300000 | 0.100000 | 0.000 | 0.680 | 0.320 | 0.3291 |
| 27479 | 0.012305 | 0.150569 | 0.837125 | 0.000000 | 0.000000 | 0.000 | 0.458 | 0.542 | 0.8074 |
27480 rows × 9 columns
pipe = roBERTa_RF
params = target_params(pipe, {
'n_jobs':-1,
'random_state':42,
'n_estimators': 500,
'classifier__min_samples_split': 15,
'classifier__min_samples_leaf': 10,
'classifier__max_depth': 15,
'classifier__class_weight': None,
'classifier__bootstrap': True
})
roBERTa_Blob_Vader_RF_opti_tot_ = trainPipelineMlFlow(
mlf_XP="Rapport",
xp_name_iter="roBERTa_Blob_Vader_RF_opti_tot",
pipeline = pipe,
X_train = X_train_compound_tot, y_train = y_train_tot, X_test = X_test_compound, y_test = y_test,
target_col = 'sentiment',
fixed_params = params,
use_opti = False,
);
XP : roBERTa_Blob_Vader_RF_opti_tot
pipeline :
Pipeline(steps=[('classifier',
RandomForestClassifier(n_estimators=500, n_jobs=-1,
random_state=42))])
params:
{'classifier__n_jobs': -1, 'classifier__random_state': 42, 'classifier__n_estimators': 500}
scores :
subset train test
metric
f1_macro 1.0 0.7502
precision_macro 1.0 0.7504
recall_macro 1.0 0.7503
Test confusion matrix:
elapsed time : 0:00:03.972592
res_fin=item
pipe = roBERTa_xgb
params = target_params(pipe, {
'classifier__n_jobs': -1,
'classifier__random_state': 42,
'classifier__gpu_id': 0,
'classifier__tree_method': 'gpu_hist',
'classifier__min_child_weight': 7,
'classifier__max_depth': 5,
'classifier__gamma': 0.1,
'classifier__colsample_bytree': 0.7
})
roBERTa_xgb_opti_tot_ = trainPipelineMlFlow(
mlf_XP="Rapport",
xp_name_iter="roBERTa_xgb_opti_tot",
pipeline = pipe,
X_train = X_train_compound_tot, y_train = y_train_tot, X_test = X_test_compound, y_test = y_test,
target_col = 'sentiment',
fixed_params = params,
use_opti = False
);
[10:22:08] WARNING: ../src/learner.cc:1061: Starting in XGBoost 1.3.0, the default evaluation metric used with the objective 'multi:softprob' was changed from 'merror' to 'mlogloss'. Explicitly set eval_metric if you'd like to restore the old behavior.
XP : roBERTa_xgb_opti_tot
pipeline :
Pipeline(steps=[('classifier',
XGBClassifier(base_score=0.5, booster='gbtree',
colsample_bylevel=1, colsample_bynode=1,
colsample_bytree=1, gamma=0, gpu_id=0,
importance_type='gain',
interaction_constraints='',
learning_rate=0.300000012, max_delta_step=0,
max_depth=6, min_child_weight=1, missing=nan,
monotone_constraints='()', n_estimators=100,
n_jobs=-1, num_parallel_tree=1,
objective='multi:softprob', random_state=42,
reg_alpha=0, reg_lambda=1, scale_pos_weight=None,
subsample=1, tree_method='gpu_hist',
validate_parameters=1, verbosity=None))])
params:
{}
scores :
subset train test
metric
f1_macro 0.8582 0.7600
precision_macro 0.8600 0.7603
recall_macro 0.8567 0.7598
Test confusion matrix:
elapsed time : 0:00:01.275500
res_fin=res_fin.append(item)
| modèle | f1_macro_val | f1_macro_test | |
|---|---|---|---|
| 0 | roBERTa_xgb_opti_ | 0.759147 | 0.759953 |
| 1 | roBERTa_Blob_Vader_RF_opti_ | 0.756699 | 0.750216 |
| 2 | roBERTa_RF_opti_ | 0.746630 | NaN |
| 3 | TfIdf_LR_opti_modif_seuil | 0.709477 | NaN |
| 4 | base_TfIdf_RF_prepro_ | 0.707919 | NaN |
| 5 | base_TfIdf_RF_prepro_opti_ | 0.706432 | NaN |
| 6 | roBERTa_RF_ | 0.705912 | NaN |
| 7 | TfIdf_LR_opti_ | 0.699877 | NaN |
| 8 | TfIdf_LR_prepro_opti_ | 0.698565 | NaN |
| 9 | base_TfIdf_RF_ | 0.669789 | NaN |
Note
Le modèle final présente un f1 macro de 0.76 sur le jeu de test.
Les résultats sont en ligne avec ceux obtenus sur le jeu de validation. Néanmoins le fait de ne pas avoir regarder les résultats sur le jeu de test avant cette étape assure que nous n’avons pas pu faire de leakage d’une manière ou d’une autre. Cette démarche émule aussi celle qui existerait dans un cas industriel où une équipe séparée serait en charge de définir un jeu de test permettant de tester le modèle dans des conditions dégradées par rapport à son domaine d’entraînement pour en assurer la stabilité en production.